﻿using System;
using System.Collections.Generic;
using System.IO;

namespace IMMeDotNet {

	partial class Host {

		#if POLLED_USB_INPUT

		public void PollInput() {
			var buf = new byte[this.wirelessAdaptorCapabilities.InputReportByteLength];
			do {
				this.wirelessAdaptorStream.Read(buf, 0, buf.Length);
				this.ProcessIncomingByte(buf[this.wirelessAdaptorCapabilities.NumberInputDataIndices]);
			} while (this.packetReadStage != PacketReadStage.Idle);
		}

		#endif

		/// <summary>
		/// Represents the part packet we're currently reading.
		/// </summary>
		private enum PacketReadStage {
			/// <summary>
			/// Nothing is being read; we're waiting for the prelude.
			/// </summary>
			Idle,
			/// <summary>
			/// We're waiting for the second byte of the prelude.
			/// </summary>
			Prelude,
			/// <summary>
			/// We're waiting for the packet length byte.
			/// </summary>
			Length,
			/// <summary>
			/// We're reading data from the packet.
			/// </summary>
			Data,
		}

		/// <summary>
		/// Stores the incoming byte data from the USB adaptor.
		/// </summary>
		private byte[] inputBuffer;

		/// <summary>
		/// Stores incoming data that forms part of a packet.
		/// </summary>
		private List<byte> incomingPacketData = new List<byte>(64);

		/// <summary>
		/// Stores the length of the incoming packet.
		/// </summary>
		private int incomingPacketLength = 0;

		/// <summary>
		/// What stage of reading a packet are we in?
		/// </summary>
		private PacketReadStage packetReadStage = PacketReadStage.Idle;

		/// <summary>
		/// Initiates an asynchronous read, used to fetch a HID input report.
		/// </summary>
		private void BeginRead() {
			this.inputBuffer = new byte[this.wirelessAdaptorCapabilities.InputReportByteLength];
			this.wirelessAdaptorStream.BeginRead(inputBuffer, 0, this.wirelessAdaptorCapabilities.InputReportByteLength, new AsyncCallback(ReadCompleted), this.inputBuffer);
		}

		/// <summary>
		/// Method used to handle data once it has been read.
		/// </summary>
		/// <param name="iResult"></param>
		private void ReadCompleted(IAsyncResult iResult) {
			var inputBuffer = (byte[])iResult.AsyncState;
			var stream = this.wirelessAdaptorStream;
			if (stream == null) return;
			try {
				stream.EndRead(iResult);
				var value = inputBuffer[this.wirelessAdaptorCapabilities.NumberInputDataIndices];

				// Now we have our value, what do we need to do with it?
				this.ProcessIncomingByte(value);

				this.BeginRead();
			} catch (OperationCanceledException) {
				// USB HID stream has been closed/disposed.
			} catch (IOException) {
				// USB adaptor has been unplugged.
			}
		}
		
		/// <summary>
		/// Process an incoming byte.
		/// </summary>
		/// <param name="value">The value to process.</param>
		private void ProcessIncomingByte(byte value) {
			switch (this.packetReadStage) {
				case PacketReadStage.Idle:
					// Currently doing nothing; is data about to start coming in?
					if (value == 0xFA) {
						this.packetReadStage = PacketReadStage.Prelude;
					}
					break;
				case PacketReadStage.Prelude:
					// Currently waiting for the second byte of the prelude (0xFB).
					if (value == 0xFB) {
						this.packetReadStage = PacketReadStage.Length;
						this.incomingPacketData.Clear();
					} else {
						this.packetReadStage = PacketReadStage.Idle;
					}
					break;
				case PacketReadStage.Length:
					// Currently waiting for the length of the packet.
					this.incomingPacketLength = value;
					this.packetReadStage = PacketReadStage.Data;
					break;
				case PacketReadStage.Data:
					// We have something meaningful to read.
					this.incomingPacketData.Add(value);
					// Is this the last byte we were waiting for in this packet?
					if (this.incomingPacketData.Count == this.incomingPacketLength) {
						this.packetReadStage = PacketReadStage.Idle;
						var packet = Packet.FromBytes(this.incomingPacketData.ToArray());
						if (!packet.IsValid) {
							throw new InvalidDataException("Incoming packet has an invalid checksum.");
						} else {
							this.PacketReceived(packet);
						}
					}
					break;
			}
		}

		/// <summary>
		/// Stores incoming packets that are used to build up messages.
		/// </summary>
		private List<Packet> incomingMessage = new List<Packet>(4);

		/// <summary>
		/// Invoked when a valid <see cref="Packet"/> has been received.
		/// </summary>
		/// <param name="packet">The <see cref="Packet"/> that has been received from the wireless adaptor.</param>
		private void PacketReceived(Packet packet) {
			// Check part numbers and part count.
			if (packet.PartNumber != this.incomingMessage.Count + 1) throw new InvalidDataException("Incoming packet has the wrong part number.");
			if (this.incomingMessage.Exists(p => p.PartCount != packet.PartNumber)) throw new InvalidDataException("Incoming packet has the wrong part count.");
			// Sounds about right; add it to the list of incoming messages.
			this.incomingMessage.Add(packet);
			// Have we received all parts of the message?
			if (incomingMessage.Count == packet.PartCount) {
				// Extract the raw parts of the message.
				var messageBytes = new List<byte>(64);
				foreach (var messagePart in this.incomingMessage) {
					messageBytes.AddRange(messagePart.Data);
				}
				this.incomingMessage.Clear();
				// Convert the message data into a Message instance.
				var message = new IncomingUsbMessage(messageBytes.ToArray());
				this.ProcessIncomingUsbMessage(message);
			}
		}

	}
}
